#region Using Statements

using System;
using System.Diagnostics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
//using Xfinity.Entities;

#endregion

namespace Xfinity.Cameras
{
    /// <summary>
    /// This is a game component that implements IUpdateable.
    /// </summary>
    public partial class Camera : IGameComponent, ICamera
    {
        #region AspectRatio
        private float aspectRatio;

        /// <summary>
        /// Gets or sets the aspect ratio.
        /// </summary>
        /// <value>The aspect ratio.</value>
        public float AspectRatio
        {
            [DebuggerStepThrough]
            get { return aspectRatio; }
            [DebuggerStepThrough]
            set
            {
                aspectRatio = value;
                Projection = Matrix.CreatePerspectiveFieldOfView(fov, aspectRatio, nearPlane, farPlane);
            }
        }
        #endregion
        #region Fov
        private float fov;

        /// <summary>
        /// Gets or sets the field of view.
        /// </summary>
        /// <value>The FOV.</value>
        public float Fov
        {
            [DebuggerStepThrough]
            get { return fov; }
            [DebuggerStepThrough]
            set
            {
                fov = value;
                Projection = Matrix.CreatePerspectiveFieldOfView(fov, aspectRatio, nearPlane, farPlane);
            }
        }
        #endregion
        #region NearPlane
        private float nearPlane;

        /// <summary>
        /// Gets or sets the near plane.
        /// </summary>
        /// <value>The near plane.</value>
        public float NearPlane
        {
            [DebuggerStepThrough]
            get { return nearPlane; }
            [DebuggerStepThrough]
            set
            {
                nearPlane = value;
                Projection = Matrix.CreatePerspectiveFieldOfView(fov, aspectRatio, nearPlane, farPlane);
            }
        }
        #endregion
        #region FarPlane
        private float farPlane;

        /// <summary>
        /// Gets or sets the far plane.
        /// </summary>
        /// <value>The far plane.</value>
        public float FarPlane
        {
            [DebuggerStepThrough]
            get { return farPlane; }
            [DebuggerStepThrough]
            set
            {
                farPlane = value;
                Projection = Matrix.CreatePerspectiveFieldOfView(fov, aspectRatio, nearPlane, farPlane);
            }
        }
        #endregion
        #region Projection
        private Matrix projection;

        /// <summary>
        /// Gets or sets the projection.
        /// </summary>
        /// <value>The projection.</value>
        public Matrix Projection
        {
            [DebuggerStepThrough]
            get { return projection; }
            [DebuggerStepThrough]
            set
            {
                projection = value;
                ViewProjection = Matrix.Multiply(viewMatrix, projection);
            }
        }
        #endregion
        #region Position
        private Vector3 position;

        /// <summary>
        /// Gets or sets the position.
        /// </summary>
        /// <value>The position.</value>
        public Vector3 Position
        {
            [DebuggerStepThrough]
            get { return position; }
            [DebuggerStepThrough]
            set
            {
                position = value;
                targetDirection = target - position;
                targetDirection.Normalize();
                Matrix m;
                Matrix.CreateLookAt(ref position, ref target, ref up, out m);
                View = m;
            }
        }
        #endregion
        #region Target
        private Vector3 target;

        /// <summary>
        /// Gets or sets the target.
        /// </summary>
        /// <value>The target.</value>
        public Vector3 Target
        {
            [DebuggerStepThrough]
            get { return target; }
            [DebuggerStepThrough]
            set
            {
                target = value;
                targetDirection = target - position;
                targetDirection.Normalize();
                pitch = Math.Asin(targetDirection.Y);
                Matrix m;
                Matrix.CreateLookAt(ref position, ref target, ref up, out m);
                View = m;
            }
        }
        double pitch = 0;
        #endregion
        #region Up
        private Vector3 up;

        /// <summary>
        /// Gets or sets the up vector.
        /// </summary>
        /// <value>The up vector.</value>
        public Vector3 Up
        {
            [DebuggerStepThrough]
            get { return up; }
            [DebuggerStepThrough]
            set
            {
                value.Normalize();
                up = value;
                Matrix m;
                Matrix.CreateLookAt(ref position, ref target, ref up, out m);
                View = m;
            }
        }
        #endregion
        #region View
        private Matrix viewMatrix;

        /// <summary>
        /// Gets or sets the view matrix.
        /// </summary>
        /// <value>The view matrix.</value>
        public Matrix View
        {
            [DebuggerStepThrough]
            get { return viewMatrix; }
            [DebuggerStepThrough]
            set
            {
                viewMatrix = value;
                Matrix m;
                Matrix.Multiply(ref viewMatrix, ref projection, out m);
                ViewProjection = m;
            }
        }
        #endregion
        #region ViewProjection
        private Matrix viewProj;

        /// <summary>
        /// Gets or sets the view projection matrix.
        /// </summary>
        /// <value>The view projection matrix.</value>
        public Matrix ViewProjection
        {
            [DebuggerStepThrough]
            get { return viewProj; }
            [DebuggerStepThrough]
            set
            {
                viewProj = value;
                frustum.Matrix = value;
            }
        }
        #endregion
        #region BoundingFrustum
        private BoundingFrustum frustum = new BoundingFrustum(Matrix.Identity);

        /// <summary>
        /// Gets or sets the frustum.
        /// </summary>
        /// <value>The frustum.</value>
        public BoundingFrustum Frustum
        {
            [DebuggerStepThrough]
            get { return frustum; }
            [DebuggerStepThrough]
            set
            {
                frustum = value;
                ViewProjection = frustum.Matrix;
            }
        }

        #endregion
        #region TargetDirection
        private Vector3 targetDirection;

        /// <summary>
        /// Gets or sets the target direction.
        /// </summary>
        /// <value>The target direction.</value>
        public Vector3 TargetDirection
        {
            [DebuggerStepThrough]
            get { return targetDirection; }
            [DebuggerStepThrough]
            set { Target = position + value; }
        }

        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="Camera"/> class.
        /// </summary>
        /// <param name="name">The name of the entity.</param>
        /// <param name="game">The Game.</param>
        public Camera(string name, Game game)
        {
            camCount++;
            this.game = game;
            //GraphicsHelper.ActiveCamera = this;
        }
        private Game game;

        /// <summary>
        /// Initializes a new instance of the <see cref="Camera"/> class.
        /// </summary>
        /// <param name="game">Game the game component should be attached to.</param>
        [DebuggerStepThrough]
        public Camera(Game game)
        {
            camCount++;
            this.game = game;
            //GraphicsHelper.ActiveCamera = this;
        }

        private static int camCount;

        /// <summary>
        /// Zoom in and out.
        /// </summary>
        /// <param name="fieldOfView">The field of view to zoom to, in half-radians. For a 90 degree FOV, send in MathHelper.ToRadians(45).</param>
        [DebuggerStepThrough]
        public void Zoom(float fieldOfView)
        {
            System.Diagnostics.Debug.Assert(fieldOfView <= MathHelper.PiOver2);
            System.Diagnostics.Debug.Assert(fieldOfView > 0);
            fov = fieldOfView;
            Projection = Matrix.CreatePerspectiveFieldOfView(fieldOfView, aspectRatio, nearPlane, farPlane);

            //create an equal mouse sensitivity independant of the field of view
            zoomFactor = Math.Sin(fieldOfView);
        }
        #region ZoomFactor
        private double zoomFactor;

        /// <summary>
        /// ZoomFactor.
        /// </summary>
        public double ZoomFactor
        {
            get { return zoomFactor; }
            set { zoomFactor = value; }
        }

        #endregion
        /// <summary>
        /// Allows the game component to perform any initialization it needs to before starting
        /// to run.  This is where it can query for any required services and load content.
        /// </summary>
        public void Initialize()
        {
            //initialise the near and far planes for the biggest view
            nearPlane = .001f;
            farPlane = 16500;

            //set Up the perspective matrix
            IGraphicsDeviceService gdm = game.Services.GetService(typeof(IGraphicsDeviceService)) as IGraphicsDeviceService;
            if (gdm != null)
            {
                aspectRatio = gdm.GraphicsDevice.Viewport.Width / (float)gdm.GraphicsDevice.Viewport.Height;
            }
            else
            {
                throw new ArgumentNullException("A IGraphicsDeviceService must be initialized.");
            }
            fov = MathHelper.PiOver4;
            projection = Matrix.CreatePerspectiveFieldOfView(fov, aspectRatio, nearPlane, farPlane);
            up = Vector3.Up;
            //set up the projection matrix
            Target = new Vector3(0, 0, 0);
            Position = new Vector3(0, 0, -50);

            viewMatrix = Matrix.CreateLookAt(position, target, Up);
            viewProj = Matrix.Multiply(viewMatrix, projection);
            frustum = new BoundingFrustum(viewProj);
            Debug.Assert(!float.IsNaN(viewMatrix.Determinant()), viewMatrix.ToString());
        }

        /// <summary>
        /// Make the camera look at a specified position.
        /// </summary>
        /// <param name="point">Where to look at.</param>
        [DebuggerStepThrough]
        public void LookAt(Vector3 point)
        {
            Target = point;
        }
        /// <summary>
        /// Move to a position in world coordinates, and look at a target in world coordinates.
        /// </summary>
        /// <param name="position">The absolute position.</param>
        /// <param name="target">The absolute target.</param>
        [DebuggerStepThrough]
        public void MoveAndRotateAbsolute(Vector3 position, Vector3 target)
        {
            Target = target;
            Position = position;
        }
        /// <summary>
        /// Move and rotate the camera.
        /// </summary>
        /// <param name="positionOffset">The relative movement.</param>
        /// <param name="targetOffset">The relative rotation.</param>
        [DebuggerStepThrough]
        public void MoveAndRotate(Vector3 positionOffset, Vector3 targetOffset)
        {
            Position += positionOffset;
            Target += targetOffset;
        }
        /// <summary>
        /// Rotates the yaw angle.
        /// </summary>
        /// <param name="angle">The angle to rotate, in radians</param>
        [DebuggerStepThrough]
        public void RotateYaw(float angle)
        {
            Target = Vector3.Transform(target - position, /*Matrix.CreateFromAxisAngle(Vector3.Up, angle)*/Matrix.CreateFromYawPitchRoll(angle, 0, 0)) + position;

        }
        /// <summary>
        /// Returns the pitch in radians.
        /// </summary>
        public double Pitch
        {
            [DebuggerStepThrough]
            get
            {
                return pitch;
            }
        }

        /// <summary>
        /// Rotates the pitch angle.
        /// </summary>
        /// <example>RotatePitch(MathHelper.ToRadians(90f));</example>
        /// <param name="angle">The angle to rotate, in radians</param>
        [DebuggerStepThrough]
        public void RotatePitch(float angle)
        {
            System.Diagnostics.Debug.Assert(!float.IsNaN(angle));
            System.Diagnostics.Debug.Assert(!float.IsInfinity(angle));
            if (Math.Abs(Pitch + angle) >= MathHelper.PiOver2 - 0.01)
            {
                return;
            }

            Vector3 rotateAbout = Vector3.Cross(targetDirection, up);
            rotateAbout.Normalize();
            Vector3 t = Vector3.Transform(targetDirection, Matrix.CreateFromAxisAngle(rotateAbout, angle)) + position;
            Target = t;

        }
        /// <summary>
        /// Rotates the roll angle.
        /// </summary>
        /// <param name="angle">The angle to rotate, in radians</param>
        [DebuggerStepThrough]
        public void RotateRoll(float angle)
        {
            Target = Vector3.Transform(target - position, Matrix.CreateRotationZ(angle)) + position;
        }


        #region IWorldViewProjection Members
        private Matrix worldMatrix = Matrix.Identity;
        public Matrix World
        {
            get
            {
                return worldMatrix;
            }
            set
            {
                worldMatrix = value;
            }
        }

        #endregion

        public void ApplyRotationMatrix(ref Matrix matrix)
        {
            Target = Vector3.Transform(target - position, matrix) + position;
        }
    }
}